聚合的目的是确保不变规则。


仓库（Repository）是以聚合为单位进行持久化的。

## 实现不变规则
我们首先来实现和改变状态有关的两个规则。

| 规则编号 | 限界上下文 | 模块   | 规则描述          | 举例                     | 影响的主要功能 |
|------|-------|------|---------------|------------------------|---------|
| R025 | 卷卷通   | 组织管理 | 试用期的员工才能被转正   | 小王已经是正式员工，不能对小王再进行转正操作 | 转正员工    |
| R026 | 卷卷通   | 组织管理 | 已经终止的员工不能再次终止 | 无                      | 终止员工    |

下面是具体代码。

```java

public class Emp extends AuditableEntity {
    // other fields
    protected EmpStatus status; // 读写

    // other getters and setters
    public EmpStatus getStatus() {
        return status;
    }
    // 转正
    void becomeRegular() {
        // 调用业务规则：试用期员工才能转正
        status = EmpStatus.REGULAR;
    }
    // 终止
    void terminate() {
        // 调用业务规则：已经终止的员工不能再次终止
        status = EmpStatus.TERMINATED;
    }
    // 实现业务规则
    private void onlyProbationCanBecomeRegular() {
        if (status != PROBATION) {
            throw new BusinessException("试用期员工才能转正！");
        }
    }
    private void shouldNotTerminateAgain() {
        if (status == TERMINATED) {
            throw new BusinessException("已经终止的员工不能再次终止！");
        }
    }
}
```

这两个规则都是业务规则，因此必须在领域层来实现。其次，由于聚合根，也就是 Emp，已经拥有了实现业务规则所需的数据，所以直接在聚合根里实现业务规则即可。

增加组织的业务规则是在领域服务里实现的，那是因为组织对象并没有所需的数据，而是要从数据库获取。

只要改变状态，相应的规则就会被调用，所以规则不会被破坏。

接下来，来实现关于技能和工作经验的不变规则。

```java

public class Emp extends AuditableEntity {
    // other fields
    protected final Map<Long, Skill> skills = new HashMap<>();
    protected final Map<Period, WorkExperience> experiences = new HashMap<>();

    // construct and other operations



    public void addSkill(Long skillTypeId, SkillLevel level, Integer duration, Long userId) {
        skillTypeShouldNotDuplicated(skillTypeId);
        Skill newSkill = new Skill(tenantId, skillTypeId, userId).setLevel(level).setDuration(duration);
        skills.put(skillTypeId, newSkill);
    }

    private void skillTypeShouldNotDuplicated(Long newSkillTypeId) {
        if (getSkill(newSkillTypeId).isPresent()) {
            throw new BusinessException("同一技能不能录入两次！");
        }
    }

    public void addExperience(Period period, String company, Long userId) {
        durationShouldNotOverlap(period);

        WorkExperience newWorkExperience = new WorkExperience(tenantId, period, LocalDateTime.now(), userId).setCompany(company);
        experiences.put(period, newWorkExperience);
    }

    private void durationShouldNotOverlap(Period newPeriod) {
        // if (experiences.stream().anyMatch(e -> e.getPeriod().overlap(newPeriod))) {
        if (experiences.values().stream().anyMatch(e -> e.getPeriod().overlap(newPeriod))) {
            throw new BusinessException("工作经验的时间段不能重叠!");
        }
    }

}

```

在增加技能 addSkill()  和增加工作经验 addExperience() 的时候校验不变规则，这样，外界就不可能破坏这些规则了。

这样的规则，某个单独的技能（Skill） 和工作经验 （WorkExperience） 对象自身是无法校验的，必须从员工聚合整体上考虑。
所以，规则的实现必须在聚合根里面完成。当然，如果聚合根没有足够的数据，需要从数据库取的话，那么这个逻辑就要放到领域服务了。

## 创建聚合的另一种做法
完成了主要的业务规则，现在来补充 添加员工（Emp） 的应用服务。在上个迭代添加组织的时候，用了 Builder 模式，这实际是工厂模式的一种实现。今天来尝试一种不同的写法。代码如下：

```java
@Service
public class EmpService {
    private final EmpRepository empRepository;
    private final EmpAssembler empAssembler;

    public EmpService(EmpRepository empRepository, EmpAssembler empAssembler) {
        this.empRepository = empRepository;
        this.empAssembler = empAssembler;
    }
    @Transactional
    public EmpResponse addEmp(CreateEmpRequest request, Long userId) {
        Emp emp = empAssembler.toEmp(request, userId);

        empRepository.save(emp);
        return empAssembler.toResponse(emp);
    }
}

```

这里用了一个叫做 assembler （装配器）的对象进行领域对象和 DTO 之间的转换。Assembler 的代码是这样的。

```java
@Component
public class EmpAssembler {
    @Resource
    private CommonOrgValidator commonOrgValidator;
    @Resource
    private EmpNumberGenerator empNumGenerator;

    Emp toEmp(CreateEmpRequest request, Long userId) {
        validateCreateRequest(request);
        String empNum = empNumGenerator.generateEmpNum(request.getTenantId());

        Emp result = new Emp(request.getTenantId(), EmpStatus.ofCode(request.getStatusCode()), userId);

        result.setEmpNum(empNum)
                .setIdNum(request.getIdNum())
                .setDob(request.getDob())
                .setOrgId(request.getOrgId())
                .setName(request.getName())
                .setGender(Gender.ofCode(request.getGenderCode()));

        request.getSkills().forEach(s -> result.addSkill(
                s.getSkillTypeId(),
                SkillLevel.ofCode(s.getLevelCode()),
                s.getDuration(),
                userId
        ));

        request.getWorkExperiences().forEach(e -> result.addExperience(
                Period.of(e.getStartDate(), e.getEndDate()),
                e.getCompany(),
                userId
        ));

        request.getPostCodes().forEach(p -> result.addEmpPost(p, userId));
        return result;
    }
    void validateCreateRequest(CreateEmpRequest request) {
        commonOrgValidator.shouldValid(request.getTenantId(),request.getOrgId());
    }
    // 将领域对象转换成 DTO
    EmpResponse toResponse(Emp emp) {
        EmpResponse empResponse = new EmpResponse();

        // TODO:
        return empResponse;
    }
}

```

Assembler 和上个迭代的 Builder 在作用上是类似的，都是用来创建领域对象。不过，assembler 用到了在应用层定义的 DTO （也就是 CreateEmpRequest），所以只能放在应用层，不能放到领域层，否则就会破坏层间依赖。


Builder 是工厂模式的一种实现，现在把 assembler 和工厂做一个比较。

工厂位于领域层，入口参数可以是基本类型、领域对象或者在领域层定义的 DTO，但不能是应用层定义的 DTO。与 assembler 相比，用工厂模式的好处是，对领域逻辑的封装更彻底一些。


比如说，上面代码的“组织应该有效”这条业务规则现在是在服务层调用的，如果用工厂的话，就会在领域层调用了。但使用工厂模式的代价就是，如果需要在领域层定义 DTO，或者采用 Builder 模式，就要写更多的代码和数据转换逻辑。

顺便说一句，“组织应该有效”这条业务规则要查询数据库，所以我们没有在领域对象中实现，而是在 OrgValidator 这个领域服务里实现的。

Assembler 位于应用层，入口参数可以是应用层定义的 DTO。使用 asembler 的优点是代码比较简洁；代价是，从理论上来说，有时领域逻辑可能稍有泄漏。对于“组织应该有效”这条业务规则，尽管规则的实现仍然在领域层，但却是从应用层调用的。不过这到底算不算领域规则的泄漏，以及泄漏得是否严重，就见仁见智了。

Assembler 的命名只是一种常见的习惯，目的是和领域层的工厂相区别。Assembler 中的逻辑也可以都写在应用服务（EmpService）里，从而取消单独的 assembler。不过，使用 assembler 可以避免庞大的应用服务类，使代码更加整洁。像 assembler 这样对 service 起辅助作用的类，一般统称为 Helper。


我们刚才说过，工厂的参数不能是应用层定义的 DTO。这个规则可以推广到整个领域层。也就是 <RedSpan>领域层中所有对象，包括领域对象、领域服务、工厂、仓库，对外暴露的方法的输入和输出参数，都只能是领域对象、基本类型，或者领域层内部定义的 DTO</RedSpan>。

分析了工厂和 assembler 的利弊，咱们就可以根据项目的具体情况和团队的偏好做出选择。不过，要注意，一个开发团队内部应该采用统一的做法。

## 新建聚合的持久化
接下来，我们看看怎样持久化聚合。在 EmpService 的 addEmp( ) 方法里，是用 empRepository.save( ) 方法对员工聚合进行持久化的。

我们之前提到过，Repository（仓库） 和传统的 DAO（数据访问对象） 虽然都用来访问数据库，但有一个重要的区别——DAO 是针对单个表的，而 Repository 是针对整个聚合的。下面我们通过代码再来理解一下。

## 包结构

